package com.riiablo.table.annotation;

import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ArrayTypeName;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Generated;
import javax.lang.model.element.Modifier;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.time.FastDateFormat;

import com.riiablo.table.Manifest;
import com.riiablo.table.Table;

import static com.riiablo.table.annotation.Constants.MANIFEST;

public class ManifestCodeGenerator {
  final Context context;
  final List<SchemaElement> schemas;

  ManifestCodeGenerator(Context context, List<SchemaElement> schemas) {
    this.context = context;
    this.schemas = schemas;
  }

  JavaFile generate() {
    TypeSpec.Builder typeSpecBuilder = TypeSpec
        .classBuilder(MANIFEST)
        .addAnnotation(newGenerated())
        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
        .addSuperinterface(Manifest.class)
        .addMethod(constructor())
        ;

    final FieldSpec INSTANCE = FieldSpec
        .builder(
            MANIFEST,
            "INSTANCE",
            Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
        .initializer("new $T()", MANIFEST)
        .build();
    typeSpecBuilder.addField(INSTANCE);

    Map<ClassName, FieldSpec> tables = new HashMap<>();
    StringBuilder tablesListBuilder = new StringBuilder();
    tablesListBuilder.append('{').append('\n');
    for (SchemaElement schema : schemas) {
      ClassName schemaName = ClassName.get(schema.element);
      FieldSpec tableFieldSpec = FieldSpec
          .builder(
              schema.tableClassName,
              schemaName.simpleName().toLowerCase(),
              Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
          .initializer("new $T($N)", schema.tableClassName, INSTANCE)
          .build();
      typeSpecBuilder.addField(tableFieldSpec);
      tablesListBuilder.append(CodeBlock.of("$N", tableFieldSpec)).append(',').append('\n');
      tables.put(schemaName, tableFieldSpec);
    }

    tablesListBuilder.append('}');
    final FieldSpec TABLES = FieldSpec
        .builder(
            ArrayTypeName.of(Table.class),
            "TABLES",
            Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
        .initializer("$L", tablesListBuilder)
        .build();
    typeSpecBuilder.addField(TABLES);

    TypeVariableName R = TypeVariableName.get("R");
    ParameterSpec table = ParameterSpec
        .builder(ParameterizedTypeName.get(ClassName.get(Table.class), R), "table")
        .build();
    ParameterSpec record = ParameterSpec
        .builder(R, "record")
        .build();
    MethodSpec.Builder inject = MethodSpec
        .methodBuilder("inject")
        .addAnnotation(Override.class)
        .addModifiers(Modifier.PUBLIC)
        .addTypeVariable(R)
        .addParameter(table)
        .addParameter(record)
        .returns(table.type)
        ;
    inject.beginControlFlow("if ($N == null)", table);
    for (SchemaElement schema : schemas) {
      if  (!schema.requiresInjection()) continue;
      ClassName schemaName = ClassName.get(schema.element);
      inject.nextControlFlow("else if ($N == $N)", table, tables.get(schemaName));
      FieldSpec castedRecord = FieldSpec
          .builder(schemaName, "r", Modifier.FINAL)
          .initializer("($T) $N", schemaName, record)
          .build();
      inject.addCode("$L", castedRecord);
      for (FieldElement field : schema.foreignKeys) {
        FieldSpec fieldSpec = tables.get(ClassName.get(field.element()));
        if (fieldSpec == null) continue;
        inject.addStatement("$N.$N = $T.$N.get($N.$N)",
            castedRecord,
            field.name(),
            MANIFEST,
            fieldSpec,
            castedRecord,
            field.foreignKeyElement.annotation.value());
      }
    }
    // Generate a default clause to throw error if table wasn't found
    // Will require cases for all tables in the manifest to be generated
    // inject.nextControlFlow("else");
    // inject.addStatement("throw new $T($S)", AssertionError.class, "table is not managed by this manifest");
    inject.endControlFlow();
    inject.addStatement("return $N", table);
    typeSpecBuilder.addMethod(inject.build());

    return JavaFile
        .builder(MANIFEST.packageName(), typeSpecBuilder.build())
        .skipJavaLangImports(true)
        .addFileComment(
            "automatically generated by $L, do not modify",
            getClass().getSimpleName())
        .build();
  }

  AnnotationSpec newGenerated() {
    return AnnotationSpec
        .builder(Generated.class)
        .addMember("value", "$S", getClass().getCanonicalName())
        .addMember("date", "$S", dateFormat().format(new Date()))
        .build();
  }

  FastDateFormat dateFormat() {
    return DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT;
  }

  MethodSpec constructor() {
    return MethodSpec
        .constructorBuilder()
        .addModifiers(Modifier.PRIVATE)
        .build();
  }
}
